package edu.objects;

import java.io.*;
import java.util.ArrayList;

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.NotPersistent;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Text;
import com.google.appengine.repackaged.com.google.common.util.Base64;
import com.google.appengine.repackaged.com.google.common.util.Base64DecoderException;

/**
 * Holds a board and associated objects, which are the LetterBag, Player.
 * 
 * The adjacency code and score calculation should probably go here?
 * 
 * @author Luke Bryan
 *
 */
@PersistenceCapable
public class Board {

	//Constants for the types of positions on the board:
	//These aren't even variable, don't need "persist".
	private static final String NORMAL = "";
	private static final String DBLWORD = "DoubleWord";
	private static final String TPLWORD = "TripleWord";
	private static final String DBLLETTER = "DoubleLetter";
	private static final String TPLLETTER = "TripleLetter";
	
	// max number of players
	private static final int NUMBER_PLAYERS = 4;
	
	//Holds [r][c] row,col letters.
	//Unfortunately arrays won't work in GAE JDO, see https://code.google.com/appengine/docs/java/datastore/jdo/relationships.html#Owned_One_to_Many_Relationships
	//@Persistent(serialized="true")
	//private Tile[][] _board;
	@NotPersistent
	private ArrayList<ArrayList<Tile>> _board;
	@Persistent(serialized="true") // to serialize the whole data, and it gae requires Text, not String, for big strings.
	private Text _boardSERIALIZED;
	
	@Persistent
	private int _currentPlayer;
	
	@Persistent(mappedBy = "owner")
	private ArrayList<Player> _playerList;
	
	@Persistent
	private LetterBag _bag;
	
	@PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;
	
	public void setKey(Key key) {
        this.key = key;
    }
	
	/**
	 * Initial the board object when the game start
	 * todo: we do no need the parameter? add new player should happen when we call
	 * addPlayer() method? 
	 * @param players
	 */
	public Board(ArrayList<Player> players) 
	{
		_playerList = players;
		try {
			_board = (ArrayList<ArrayList<Tile>>) fromString(_boardSERIALIZED.toString());
		} catch (Exception e) {
			e.printStackTrace();
			_board = new ArrayList<ArrayList<Tile>>();
		}
		//
		//Have to manually make arraylist into 2d "array", because GAE doesn't support those.
		for (int r=0; r<15; r++) {
			ArrayList<Tile> therow = new ArrayList<Tile>();
			for (int c=0; c<15; c++) {
				therow.add(null);
			}
			_board.add(therow);
		}
		try {
			_boardSERIALIZED = new Text(toSerString(_board));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		_bag = new LetterBag();;
	}
	
	/**
	 * initial a new Board object with empty player
	 * ths caller can call the addPlayer to add new player to
	 * the board
	 */
	public Board ()
	{
		//no player constructor.
		this(new ArrayList<Player>());
	}
	
	/**
	 * Checks if move is valid. returns point value if move succeeded, or -1 for fail.
	 * @param move - the position 
	 * @return valid move point if succeeded, otherwise -1 for fail
	 */
	public int validateMove(String move)
	{
		ArrayList<PositionTile> tiles = new ArrayList<PositionTile>();
		String[] values = move.split(" ");
		for (String value : values) { // add positiontile of each played letter.
			tiles.add(new PositionTile(value));
		}
		int points = 0;
		// make sure all in one row/col
		int row = tiles.get(0).getR();
		int col = tiles.get(0).getC();
		boolean isrow = true;
		boolean iscol = true;
		//but not if another row/col is not equal. so check:
		for (PositionTile tile : tiles) {
			if (tile.getC() != col) {
				iscol = false;
			}
			if (tile.getR() != row) {
				isrow = false;
			}
			if (_board.get(tile.getR()).get(tile.getC()) != null) {
				isrow = false;
				iscol = false;
				//Not a row or col, bad move!
			}
		}

		// get the words they added, all words that are part of new letters.
		if (iscol) {
			for (PositionTile tile : tiles) {
				points += getWordScore(tiles, tile, true);
			}
			
		} else if (isrow) {
			for (PositionTile tile : tiles) {
				points += getWordScore(tiles, tile, false);
			}
		} else {
			points = -1; //fail
		}
		placeTiles(tiles);
		_currentPlayer++;
		return points;
	}
	
	/**
	 * Place tiles on board.
	 * @param tiles
	 */
	private void placeTiles(ArrayList<PositionTile> tiles) {
		for (PositionTile tile : tiles) {
			_board.get(tile.getR()).set(tile.getC(), new Tile(tile.getLetter(), tile.isBlank()));
			try {
				_boardSERIALIZED = new Text(toSerString(_board));
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	/**
	 * Checks played word and adjacent letters, across or up/down from given tile,
	 *  returns score or -1 for fail.
	 * @param tiles - 
	 * @param tile - The position to start from.
	 * @param leftright - true if words are from left to right
	 */
	private int getWordScore(ArrayList<PositionTile> tiles, PositionTile tile,
			boolean leftright) {
		StringBuilder word = new StringBuilder(tile.getLetter());
		int points = 0;
		if (leftright) {
			int r = tile.getR();
			int c = tile.getC()-1;
			while (c>=0 && _board.get(r).get(c) !=(null)) { // letters to left
				word.insert(0, _board.get(r).get(c).getLetter());
				c--;
			}
			c=tile.getC()+1;
			while (c<=14 && _board.get(r).get(c) != null) { // letters to right
				word.append(_board.get(r).get(c).getLetter());
				c++;
			}
		} else {
			int r = tile.getR()-1;
			int c = tile.getC();
			while (r>=0 && _board.get(r).get(c) !=null) { // letters above
				word.insert(0, _board.get(r).get(c).getLetter());
				r--;
			}
			r = tile.getR()+1;
			while (r<=14 && _board.get(r).get(c) != null) { // letters below
				word.append(_board.get(r).get(c).getLetter());
				r++;
			}
		}
		//Word found from adjacent letters, check and get score:
		if (isWord(word.toString())) {
			for (int i=0; i<word.length(); i++) {
				//Got to be a better way to do this, like static method in letterbag?
				points += new LetterBag().getPointValue(word.charAt(i));
			}
			points = getScore(tile, points-new LetterBag().getPointValue(tile.getLetter()));
		}
		return points;
	}

	/**
	 * Returns the score of a word, given player's added letters,
	 * and the sum score of other letters.
	 * @param tiles
	 * @param otherLtrScore
	 * @return
	 */
	private int getScore(ArrayList<PositionTile> tiles, int otherLtrScore) {
		int multiplier = 1;
		int score = 0;
		for (PositionTile tile : tiles) {
			String type = positionType(tile.getR(), tile.getC());
			if (type.equals(DBLWORD)) {
				multiplier = 2;
				score += tile.getValue();
			} else if (type.equals(TPLWORD)) {
				multiplier = 3;
				score += tile.getValue();
			} else if (type.equals(DBLLETTER)) {
				score += 2*tile.getValue();
			} else if (type.equals(TPLLETTER)) {
				score += 3*tile.getValue();
			}
		}
		return multiplier*(score+otherLtrScore);
	}
	
	private int getScore(PositionTile tile, int otherLtrScore) {
		ArrayList<PositionTile> tiles = new ArrayList<PositionTile>();
		return getScore(tiles,otherLtrScore);
	}
	
	/**
	 * This should look for the word in the dictionary file.
	 * @param string
	 * @return
	 */
	private boolean isWord(String string) {
		// TODO Auto-generated method stub
		return true;
	}
	
	/**
	 * Returns the list of current players.
	 * @return ArrayList<Player> - the player list, which shows the current players
	 */
	public ArrayList<Player> getPlayerList() {
		return _playerList;
	}
	
	/**
	 * Returns player-object of current player.
	 * @return Player - player-object of current player
	 */
	public int getCurrentTurn() {
		return _currentPlayer;
	}
	
	/**
	 * Sets the next player, after a turn or trade-in.
	 */
	private void setCurrentTurn() {
		//TODO:Set next player code here?
	}
	
	/**
	 * Add a player to board.
	 * @param playerName - the name of the user player
	 * @return true - if the user added to the player list successfully, otherwise false
	 */
	public boolean addPlayer(String playerName) {
		// set return result
		boolean result = false;
		
		// check if the player list is full (over than 4)
		if(this._playerList.size() < 4)
		{
			// add a new player object to _playerList
			this._playerList.add(new Player(playerName, this));
			// set result to true
			result = true;
		}
		// if the player list is full then we are going to return false
		else
		{
			result = false;
		}
		return result;
	}
	
	/**
	 * The method will going to check if the game comes out a winner,
	 * returns the player-object of the winner, if applicable, otherwise null.
	 * @return Player - the player-object of the winner, if applicable, otherwise null
	 */
	public Player checkWinner() {
		return null;//no winner.
	}
	
	/**
	 * Shows a html
	 */
	public String toString() {
		//_board.get(0); watch out, first get will cause error :(
		StringBuilder html = new StringBuilder("" +
				"<style>.DoubleWord {background-color:red;}" +
				".DoubleLetter {background-color:blue;}" +
				".TripleLetter {background-color:green;}" +
				".TripleWord {background-color:orange;}" +
				".square {border-style: solid; border-width: 1px; height: 40px; text-align: center; width: 40px;}</style>");
		html.append("<table style='background-color: #EBFF95;' >\n");
		for (int r=0; r<15; r++) {
			html.append("<tr>\n");
			for (int c=0; c<15; c++) {
				html.append("<td class=\"square "+positionType(r, c)+"\" id=\""+r+","+c+"\">");
				if (false/*_board.get(r).get(c) != null*/) {
					//html.append(_board.get(r).get(c));
				} else {
					html.append("&nbsp;");
				}
				html.append("</td>");
			}
			html.append("\n</tr>\n");
		}
		html.append("</table>");
		return html.toString();
	}
	
	/**
	 * Given a row and col, tells the scoring type of this position on the board.
	 * @param r - row
	 * @param c - column 
	 * @return string description (normal or double/triple letter/word).
	 */
	private String positionType(int r, int c) {
		String type;
		if (r==c && r>0 && r<5) {
			type = DBLWORD;
		} else if (r==c && r>9 && r<14) {
			type = DBLWORD;
		} else if (r==7 && c==7) {//center.
			type = DBLWORD;
		} else if (r+c==14 && ((r>0 && r<5) || (r>9 && r<14))) {
			type = DBLWORD;
		} else if (Math.abs(r-7)==1 && Math.abs(c-7)==1) {//one away from center
			type = DBLLETTER;
		} else if (Math.abs(r-7)==2 && Math.abs(c-7)==2) {//2 away.
			type = TPLLETTER;
		} else if (Math.abs(r-7)==7 && Math.abs(c-7)==7) {
			type = TPLWORD;
		} else if (Math.abs(r-7)==1 && Math.abs(c-7)==5) {
			type = DBLLETTER;
		} else if (Math.abs(r-7)==5 && Math.abs(c-7)==1) {
			type = DBLLETTER;
		} else if (Math.abs(r-7)==0 && Math.abs(c-7)==4) {
			type = DBLLETTER;
		} else if (Math.abs(r-7)==4 && Math.abs(c-7)==0) {
			type = DBLLETTER;
		} else if (Math.abs(r-7)==2 && Math.abs(c-7)==6) {
			type = TPLLETTER;
		} else if (Math.abs(r-7)==6 && Math.abs(c-7)==2) {
			type = TPLLETTER;
		} else if (Math.abs(r-7)==7 && Math.abs(c-7)==0) {
			type = TPLWORD;
		} else if (Math.abs(r-7)==0 && Math.abs(c-7)==7) {
			type = TPLWORD;
		} else if (Math.abs(r-7)==4 && Math.abs(c-7)==7) {
			type = DBLLETTER;
		} else if (Math.abs(r-7)==7 && Math.abs(c-7)==4) {
			type = DBLLETTER;
		} else {
			type = NORMAL;
		}
		return type;
	}
	
	/*
	 *  Two functions based on http://stackoverflow.com/questions/917604/decode-base64-string-java-5 
	 */
    /** Read the object from Base64 string. 
     * @throws Base64DecoderException */
    private static Object fromString( String s ) throws IOException, ClassNotFoundException, Base64DecoderException {
        byte [] data = Base64.decode( s );
        ObjectInputStream ois = new ObjectInputStream( 
                                        new ByteArrayInputStream(  data ) );
        Object o  = ois.readObject();
        ois.close();
        return o;
    }
    /** Write the object to a Base64 string. */
    private static String toSerString( Serializable o ) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream( baos );
        oos.writeObject( o );
        oos.close();
        return new String( Base64.encode( baos.toByteArray() ) );
    }

	public LetterBag getBag() {
		return _bag;
	}

}
